/*
 * Created by zhangzxiang91@gmail.com on 2021/09/23.
 */
package com.perfma.util.service;

import com.perfma.util.model.FileInfo;
import com.perfma.util.model.StorageProperties;
import com.perfma.util.tool.TraceContext;
import io.minio.*;
import io.minio.errors.ErrorResponseException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 官方文档：https://docs.min.io/docs/java-client-api-reference.html
 *
 * @author zhangzxiang91@gmail.com
 * @date 2021/09/23.
 */
public class MinioStorageServiceImpl extends AbstractStorageService {

    private static final String FILE_NAME_KEY = "filename";

    private final ExecutorService executor = Executors.newFixedThreadPool(1);
    private final String endpoint;
    private final String externalEndpoint;
    private final String bucketName;
    private final long partSize;
    private final MinioClient client;

    public MinioStorageServiceImpl(StorageProperties properties) {
        super(properties);
        StorageProperties.MinioConfig config = properties.getMinio();

        if (StringUtils.isBlank(config.getEndpoint())) {
            throw new IllegalArgumentException("Minio: 'storage.minio.endpoint' is empty");
        }
        if (StringUtils.isBlank(config.getAccessKey())) {
            throw new IllegalArgumentException("Minio: 'storage.minio.accessKey' is empty");
        }
        if (StringUtils.isBlank(config.getSecretKey())) {
            throw new IllegalArgumentException("Minio: 'storage.minio.secretKey' is empty");
        }
        if (StringUtils.isBlank(config.getBucketName())) {
            throw new IllegalArgumentException("Minio: 'storage.minio.bucketName' is empty");
        }

        if (!config.getEndpoint().contains("://")) {
            this.endpoint = "http://" + config.getEndpoint();
        } else {
            this.endpoint = config.getEndpoint();
        }

        if (StringUtils.isBlank(config.getExternalEndpoint())) {
            this.externalEndpoint = endpoint;
        } else {
            if (!config.getExternalEndpoint().contains("://")) {
                this.externalEndpoint = "http://" + config.getExternalEndpoint();
            } else {
                this.externalEndpoint = config.getExternalEndpoint();
            }
        }

        try {
            this.bucketName = config.getBucketName();
            this.partSize = config.getPartSize();
            this.client = MinioClient.builder().endpoint(endpoint).credentials(config.getAccessKey(), config.getSecretKey()).build();

            BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
            if (!this.client.bucketExists(bucketExistsArgs)) {
                if (!config.isBucketAutoCreate()) {
                    throw new IllegalStateException("The bucket '" + bucketName + "' does not exist.");
                }

                MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build();
                try {
                    this.client.makeBucket(makeBucketArgs);
                } catch (ErrorResponseException e) {
                    if (!e.errorResponse().code().equals("BucketAlreadyOwnedByYou")) {
                        throw e;
                    }
                }
            }
//            log.info("创建存储服务[Minio]客户端实例, bucketName:{} address:{}", config.getBucketName(), config.getEndpoint());
        } catch (Throwable e) {
            throw new IllegalStateException("创建存储服务[Minio]客户端实例失败, address:" + config.getEndpoint(), e);
        }
    }

    @Override
    protected String doUpload(String fileKey, UploadInputStream stream, long objectSize, String originalName) throws Exception {
        if (StringUtils.isBlank(fileKey)) {
            fileKey = genFileKey();
        }
        upload(fileKey, stream, objectSize, originalName);
        return fileKey;
    }

    @Override
    protected String doAppendUpload(String fileKey, UploadInputStream stream, long objectSize, String originalName) throws Exception {
        if (StringUtils.isBlank(fileKey)) {
            return doUpload(null, stream, objectSize, originalName);
        }

        FileInfo fileInfo = doGetFileInfo(fileKey);
        if (fileInfo == null) {
            return doUpload(fileKey, stream, objectSize, originalName);
        }

        Map<String, String> metadata = new HashMap<>();
        metadata.put(FILE_NAME_KEY, fileInfo.getFileName());
        if (fileInfo.getFileSize() < ObjectWriteArgs.MIN_MULTIPART_SIZE) {
            // 上传文件小于5M，则先下载到本地，合并文件后再上传
            File tmpFile = new File(FileUtils.getTempDirectoryPath(), UUID.randomUUID().toString());
            try {
                client.downloadObject(DownloadObjectArgs.builder().bucket(bucketName).object(fileKey).filename(tmpFile.getAbsolutePath()).build());
                try (FileOutputStream fos = new FileOutputStream(tmpFile, true)) {
                    IOUtils.copy(stream, fos, 8192);
                }

                client.uploadObject(
                        UploadObjectArgs.builder().bucket(bucketName).object(fileKey).userMetadata(metadata).filename(tmpFile.getAbsolutePath())
                                .build());
            } finally {
                FileUtils.deleteQuietly(tmpFile);
            }
        } else {
            // 上传文件大于5M，使用composeObject方式合并上传
            String uuid = UUID.randomUUID().toString();
            String appendFileKey = fileKey + "_tmp_appendFile_" + uuid;
            String composeFileKey = fileKey + "_tmp_composeFile_" + uuid;
            try {
                this.client.putObject(PutObjectArgs.builder().bucket(bucketName).object(appendFileKey).stream(stream, objectSize, partSize).build());

                List<ComposeSource> sources = new ArrayList<>();
                sources.add(ComposeSource.builder().bucket(bucketName).object(fileKey).build());
                sources.add(ComposeSource.builder().bucket(bucketName).object(appendFileKey).build());

                client.composeObject(ComposeObjectArgs.builder().bucket(bucketName).object(composeFileKey).sources(sources).build());
                client.copyObject(CopyObjectArgs.builder().bucket(bucketName).object(fileKey).userMetadata(metadata)
                        .source(CopySource.builder().bucket(bucketName).object(composeFileKey).build()).build());
            } finally {
                // 删除文件追加产生的临时文件
                TraceContext.Trace trace = TraceContext.get().getLogTrace();
                executor.submit(() -> {
                    TraceContext.get().setTrace(trace);
                    Arrays.asList(appendFileKey, composeFileKey).forEach(deleteKey -> {
                        RemoveObjectArgs.Builder builder = RemoveObjectArgs.builder().bucket(bucketName).object(deleteKey);
                        try {
                            client.removeObject(builder.build());
                        } catch (Throwable e) {
//                            log.warn("AppendUpload:RemoveObject[{}] error: {}", deleteKey, e.getMessage());
                            e.printStackTrace();
                        }
                    });
                });
            }
        }
        return fileKey;
    }

    @Override
    protected void doTruncateUpload(String fileKey, UploadInputStream stream, long objectSize, String originalName) throws Exception {
        upload(fileKey, stream, objectSize, originalName);
    }

    private void upload(String fileKey, UploadInputStream stream, long objectSize, String originalName) throws Exception {
        String filename = Optional.ofNullable(StringUtils.trimToNull(originalName)).orElse(FilenameUtils.getName(fileKey));
        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Disposition", "attachment;filename=\"" + filename + "\"");
        Map<String, String> metadata = new HashMap<>();
        metadata.put(FILE_NAME_KEY, filename);

        PutObjectArgs.Builder builder = PutObjectArgs.builder().bucket(bucketName).object(fileKey);
        builder.stream(stream, objectSize, partSize);
        builder.userMetadata(metadata);
        builder.headers(headers);
        this.client.putObject(builder.build());
    }

    @Override
    protected long doDownload(String fileKey, OutputStream os, Long offset, Long length) throws Exception {
        GetObjectArgs.Builder builder = GetObjectArgs.builder().bucket(bucketName).object(fileKey);
        if (offset != null && length != null) {
            builder.offset(offset).length(length);
        }
        try (GetObjectResponse response = client.getObject(builder.build())) {
            return IOUtils.copy(response, os);
        }
    }

    @Override
    protected void doDeleteFile(String fileKey) throws Exception {
        client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileKey).build());
    }

    @Override
    protected FileInfo doGetFileInfo(String fileKey) throws Exception {
        StatObjectResponse response;
        try {
            response = client.statObject(StatObjectArgs.builder().bucket(bucketName).object(fileKey).build());
        } catch (ErrorResponseException e) {
            if (e.errorResponse().code().equals("NoSuchKey")) {
                return null;
            }
            throw e;
        }

        FileInfo fileInfo = new FileInfo();
        fileInfo.setFileKey(fileKey);
        fileInfo.setFileName(response.userMetadata().getOrDefault(FILE_NAME_KEY, FilenameUtils.getName(fileKey)));
        fileInfo.setFileSize(response.size());
        fileInfo.setCrc32(getCRC32(response.etag()));
        return fileInfo;
    }
}
